namespace imichaelmiers.wp7.security.cryptography.PasswordProtectedStorage
{
    using System;
    using System.IO;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.Security.Cryptography;

    ///<summary>
    /// READ WARNINGS AT  <see cref="PasswordProtectedStorage"/>           
    /// and this class before using.                                       
    /// Provides an encrypted and authenticated opaque container for objects
    /// serializable with DataContractSerializer. The stored object is neither
    /// readable nor modifiable without the provided password, even when 
    /// written to disk. This storage object is itself serializable with 
    /// DataContractSerializer.
    /// <example>
    ///     <code>
    ///     // DO NOT HARD CODE PASSWORDS IN SOURCE. FOR EDUCATIONAL
    ///     // PURPOSES ONLY.
    ///     // USING STRINGS TO STORE THE PASSWORD IS ALSO ILL-ADVISED
    ///     var foo = new CryptographicallySealedObject%ltstring%gt("password", "foo");
    ///     textBox1.Text = foo.getData("password"); 
    ///     //textBox1 should now contain the string "foo"
    ///     </code>
    /// </example>
    /// </summary>
    /// <remarks>
    /// Note: if you wish to encrypt multiple objects under the same password,
    /// it is far more efficient to either hand this class a list of the
    /// objects or serialize them all to a MemoryStream and then hand this 
    /// class the backing byte array. For small classes especially, 
    /// key generation will take far longer than encryption time because
    /// keys are generated fresh on any call to a constructor 
    /// or getData.
    /// <para>
    /// Implementation details follow:
    /// Effectively this object stores  
    /// {Salts,HMAC_SHA_256(AES_CBC_256(data,IV,k1)||IV)k2} where k1 and k2 
    /// are keys and || denotes concatenation.
    /// </para>
    /// <para>
    /// The keys are derived from the password using Rfc2898DeriveBytes 
    /// with 1000 iterations and a random 256 bit salt provided by 
    /// RNGCryptoServiceProvider.
    /// </para>
    /// <para>
    /// Data is encrypted using AES with a 256 bit key, CBC Mode and 
    /// a random IV provided by RNGCryptoServiceProvider.
    /// </para>
    /// <para>
    /// The encrypted data and IV are concatenated and authenticated
    /// using HMACSHA256. 
    /// </para>
    /// <para>
    /// These are the managed code implementations of the crypto functions
    /// and as such are not FIPS certified. 
    /// </para>
    /// </remarks>
    /// <typeparam name="T">The type to protect.</typeparam>
    [DataContract]
    public class PasswordProtectedContainer<T>
    {
        // Silverlight does not allow serialization of private members  
        // because of access restrictions imposed by sandboxing. Hence 
        // these members must remain public. 

        /// <summary>
        /// Public for serialization purposes only.
        /// </summary>
        [DataMember]
        public readonly byte[] _EncryptedData;

        /// <summary>
        /// Public for serialization purposes only.
        /// </summary>
        [DataMember]
        public readonly byte[] _MessageAuthenticationCode;

        /// <summary>
        /// Public for serialization purposes only.
        /// </summary>
        [DataMember]
        public readonly PublicCryptographicMetaData _MetaData;

        private const string NullDataMessage = "Data cannot be null.";
        #region constructors

        /// <summary>
        /// <see cref="PasswordProtectedContainer(byte[],T)"/>
        /// X WARNING don't use strings to store passwords    
        ///  <see cref="PasswordProtectedStorage"/> warning  
        ///  5 for why.                                      
        /// </summary>
        public PasswordProtectedContainer(string password, T data) :
            this(new SymmetricKeys(password), data) { }

        /// <summary>
        /// Encrypts and authenticates the provided serializable object 
        /// using the provided password. The object must be serializable
        /// via DataContractSerializer.
        /// </summary>
        /// <param name="password">The byte array storing the password.</param>
        /// <param name="data">The data to protect.</param>
        public PasswordProtectedContainer(byte[] password, T data) :
            this(new SymmetricKeys(password), data) { }

        /// <summary>
        /// Private constructor for use by this class only.
        /// Warning: key will be disposed of on use.
        /// </summary>
        /// <param name="key"> The SymetricKeys object for 
        /// encrypting/authenticating data. Will be disposed of on use.</param>
        /// <param name="data">The data to protect</param>
        private PasswordProtectedContainer(SymmetricKeys key, T data)
        {
            if (null == data)
            {
                throw new ArgumentNullException(NullDataMessage);
            }

            using (SymmetricKeys keys = key)
            {
                this._MetaData = keys.MetaData;
                using (SymmetricAlgorithmWrapper cipher = SetUpCipher(keys))
                {
                    this._EncryptedData = EncryptData(data, cipher.Cipher);
                }

                this._MessageAuthenticationCode = GenerateMac(keys);
            }
        }
        #endregion
        #region public methods
        /// <summary>
        /// Retrieves the data when given the password as a string
        /// <see cref="GetData(byte[])"/>
        /// <para>
        ///  WARNING don't use strings to store passwords    
        /// <see cref="PasswordProtectedStorage"/> warning  
        ///  5 for why.                                      
        /// </para>
        /// </summary>
        public T GetData(string password)
        {
            using (SymmetricKeys keys = 
                new SymmetricKeys(password, _MetaData))
            {
                return GetData(keys);
            }
        }

        /// <summary>
        /// Retrieves the data when given the password as a byte array.
        /// </summary>
        /// <param name="password">The password as a byte array.</param>
        /// <returns>The data</returns>
        /// <exception cref="CryptographicException">
        /// If the password is incorrect or the data was tampered with.
        /// </exception>
        public T GetData(byte[] password)
        {
            using (SymmetricKeys keys = 
                new SymmetricKeys(password, _MetaData))
            {
                return GetData(keys);
            }
        }
        #endregion
        #region protected methods
        /// <summary>
        /// Overidable function for handling object serialization.
        /// </summary>
        /// <param name="data">The data to serialize</param>
        /// <param name="dest">The CryptoStream to serialize to.</param>
        protected virtual void Serialize(T data, CryptoStream dest)
        {
            DataContractSerializer serializer = 
                new DataContractSerializer(typeof(T));
            serializer.WriteObject(dest, data);
        }

        /// <summary>
        /// Overridable function for handling object deserialization.
        /// </summary>
        /// <param name="source">The CryptoStream to deserialize from </param>
        /// <returns>The object</returns>
        protected virtual T DesSerialize(CryptoStream source)
        {
            DataContractSerializer serializer =
                new DataContractSerializer(typeof(T));
            return (T)serializer.ReadObject(source);
        }
        #endregion
        #region Private Methods

        private T DecryptData(byte[] data, SymmetricAlgorithm cipher)
        {
            using (MemoryStream stream = new MemoryStream(_EncryptedData))
            using (ICryptoTransform t = cipher.CreateDecryptor())
            using (CryptoStream cs = new CryptoStream(stream, t, CryptoStreamMode.Read))
            {
                return DesSerialize(cs);
            }
        }

        private byte[] EncryptData(T data, SymmetricAlgorithm cipher)
        {
            using (MemoryStream stream = new MemoryStream())
            using (ICryptoTransform t = cipher.CreateEncryptor())
            using (CryptoStream cs = new CryptoStream(stream, t, CryptoStreamMode.Write))
            {
                Serialize(data, cs);
                cs.FlushFinalBlock();
                return stream.ToArray();
            }
        }

        private byte[] GenerateMac(SymmetricKeys keys)
        {
            using (HMACSHA256 mac = new HMACSHA256())
            {
                keys.SetMacKey(mac);
                return mac.ComputeHash(this._EncryptedData.Concat(_MetaData.IV).ToArray());
            }
        }

        private T GetData(SymmetricKeys keys)
        {
            if (ValidateMessageAuthenticationCode(keys))
            {
                using (SymmetricAlgorithmWrapper cipher = SetUpCipher(keys))
                {
                    return DecryptData(this._EncryptedData, cipher.Cipher);
                }
            }
            else
            {
                throw new CryptographicException("Either your password is" +
                    " wrong or your data has been tampered with: Invalid" +
                    " message authentication code.");
            }
        }

        private SymmetricAlgorithmWrapper SetUpCipher(SymmetricKeys keys)
        {
            AesManaged aes = new AesManaged();
            keys.SetEncryptionKey(aes);
            aes.IV = _MetaData.IV;
            return new SymmetricAlgorithmWrapper(aes);
        }

        private bool ValidateMessageAuthenticationCode(SymmetricKeys keys)
        {       
            byte[] generatedMac = GenerateMac(keys);

            // We want to prevent a timing attack on the mac verification,
            // so we make this take constant time.
            bool result = (generatedMac.Length == _MessageAuthenticationCode.Length);
            for (int i = 0; i < _MessageAuthenticationCode.Length; i++)
            {
                result = result & (_MessageAuthenticationCode[i] == generatedMac[i]);
            }

            return result;
        }
        #endregion
    }
}